package za.org.jcicada.katcp;

/**
 * Server.java
 *
 * @author Simon Cross
 *
 * Created on October 13, 2008, 13:45
 */

import za.org.jcicada.katcp.BaseServer;
import za.org.jcicada.katcp.DeviceLogger;
import za.org.jcicada.katcp.IRequestHandler;
import za.org.jcicada.katcp.ISamplingStrategy;
import za.org.jcicada.katcp.ISensor;
import za.org.jcicada.katcp.Message;

import java.nio.channels.SocketChannel;
import java.util.TreeSet;
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;

/**
 * Represents a KATCP device which implements the standard katcp request
 * methods.
 *
 * Implements some standard messages on top of DeviceServerBase.
 * 
 * Inform messages handled are:
 * - version (sent on connect)
 * - build-state (sent on connect)
 * - log (via self.log.warn(...), etc)
 * - disconnect
 * 
 * Requests handled are:
 * - halt
 * - help
 * - log-level
 * - restart (if self.schedule_restart(...) implemented)
 * - sensor-list
 * - sensor-sampling
 * - watchdog
 * 
 * Unhandled standard messages are:
 * ?configure
 * ?mode
 * 
 * Subclasses can define the String array VERSION_INFO to set the server
 * name, major and minor version numbers. EXTRA_VERSION_INFO can
 * be defined to give a string describing releases within a
 * version, for example "rc1".
 * 
 * Subclasses should call addRequestHandler(...) and addSensor(...) in
 * their constructors to register the request and sensors they support.
 */
public class Server extends BaseServer {

    /**
     * Interface version information.
     */
    public String[] VERSION_INFO = new String[] {"device_stub", "0", "1" };

    /**
     * Device server build / instance information.
     */
    public String[] BUILD_INFO = new String[] {"name", "0", "1", "" };

    /**
     * DeviceLogger instance for sending log messages to the client.
     */
    public final DeviceLogger log;

    /**
     * Create a new Server object.
     *
     * The constructor is where request handler and sensors
     * are registered with calls to addSensor() and addRequestHandler().
     *
     * @param host  Host name to listen on.
     * @param port  Port to listen on.
     */
    public Server(String host, int port) {
        super(host, port);
        log = new DeviceLogger(this);

        addRequestHandler(new HaltRequest(this));
        addRequestHandler(new HelpRequest(this));
        addRequestHandler(new LogLevelRequest(this));
        addRequestHandler(new RestartRequest(this));
        addRequestHandler(new SensorListRequest(this));
        addRequestHandler(new SensorSamplingRequest(this));
        addRequestHandler(new WatchdogRequest(this));
    }

    /**
     * Inform client of build state and version on connect.
     */
    public void onClientConnect(SocketChannel sc)
    throws MessageSendException {
        inform(sc, "version", version());
        inform(sc, "build-state", buildState());
    }

    /**
     * Inform client it is about to be disconnected.
     */
    public void onClientDisconnect(SocketChannel sc, String reason)
    throws MessageSendException {
        inform(sc, "disconnect", reason);
    }

    /**
     * Return a version string of the form type-major.minor.
     *
     * If full is True, optionally add further version information
     * as a suffix.
     *
     * @return The server API version string.
     */
    public String version() {
        return VERSION_INFO[0] + "-" + VERSION_INFO[1]
                               + "." + VERSION_INFO[2];
    }

    /**
     * Return a build state string in the form name-major.minor[(a|b|rc)n].
     *
     * @return The server instance build state.
     */
    public String buildState() {
        return BUILD_INFO[0] + "-" + BUILD_INFO[1]
                             + "." + BUILD_INFO[2] + BUILD_INFO[3];
    }

    /**
     * Schedule a restart.
     * 
     * Unimplemented by default since this depends on the details
     * of how subclasses choose to manage the .run() and .stop()
     * methods.
     */
    public void scheduleRestart() {
        throw new UnsupportedOperationException(
            "Server restarts not implemented for this device."
        );
    }

    // request class base

    abstract class Request implements IRequestHandler {
        protected final String name;
        protected final String help;
        protected final Server server;

        public Request(Server server, String name, String help) {
            this.server = server;
            this.name = name;
            this.help = help;
        }

        public String getName() {
            return name;
        }

        public String getHelp() {
            return help;
        }
        
        public abstract Message handle(SocketChannel sc, Message msg)
        throws Exception;
    }

    // request implementations

    class HaltRequest extends Request {
        public HaltRequest(Server server) {
            super(server, "halt", "Halt the server.");
        }
        public Message handle(SocketChannel sc, Message msg) {
            server.stop();
            // this message makes it through because stop
            // only registers in .run(...) after the reply
            // has been sent.
            return Message.reply("halt", "ok");
        }
    }

    class HelpRequest extends Request {
        public HelpRequest(Server server) {
            super(server, "help", "Return help on the available request methods.");
        }
        public Message handle(SocketChannel sc, Message msg)
        throws BaseServer.MessageSendException {
            String[] args = msg.getArguments();
            if (args.length == 0) {
                for (String name: new TreeSet<String>(requestHandlers.keySet())) {
                    IRequestHandler h = requestHandlers.get(name);
                    server.inform(sc, "help", name, h.getHelp());
                }
                int numHandlers = requestHandlers.size();
                return Message.reply("help", "ok", String.valueOf(numHandlers));
            }
            else {
                String name = args[0];
                if (requestHandlers.containsKey(name)) {
                    IRequestHandler h = requestHandlers.get(name);
                    server.inform(sc, "help", name, h.getHelp());
                    return Message.reply("help", "ok", "1");
                }
                return Message.reply("help", "fail", "Unknown request method.");
            }
        }
    }

    class LogLevelRequest extends Request {
        public LogLevelRequest(Server server) {
            super(server, "log-level", "Query or set the current logging level.");
        }
        public Message handle(SocketChannel sc, Message msg) {
            String[] args = msg.getArguments();
            if (args.length != 0) {
                server.log.setLogLevelByName(msg.arguments[0]);
            }
            DeviceLogger.Level level = server.log.getLogLevel();
            return Message.reply("log-level", "ok", level.katcpName());
        }
    }

    class RestartRequest extends Request {
        public RestartRequest(Server server) {
            super(server, "restart", "Restart the device server.");
        }
        public Message handle(SocketChannel sc, Message msg) {
            server.scheduleRestart();
            return Message.reply("restart", "ok");
        }
    }

    class SensorListRequest extends Request {
        public SensorListRequest(Server server) {
            super(server, "sensor-list", "Request the list of sensors.");
        }

        public Message handle(SocketChannel sc, Message msg)
        throws BaseServer.MessageSendException {
            String[] args = msg.getArguments();
            ISensor sensor;
            List<String> msgArgs = new LinkedList<String>();

            if (args.length == 0) {
                for (String name: new TreeSet<String>(sensors.keySet())) {
                    sensor = sensors.get(name);
                    sensor.read();

                    msgArgs.clear();
                    msgArgs.add(sensor.getName());
                    msgArgs.add(sensor.getType().katcpName());
                    msgArgs.add(sensor.getDescription());
                    msgArgs.add(sensor.getUnits());
                    for (String param: sensor.getParameters()) {
                        msgArgs.add(param);
                    }
         
                    server.inform(sc, new Message(Message.Type.INFORM,
                                        "sensor-list",
                                        msgArgs.toArray(new String[0])));
                }
                int numSensors = sensors.size();
                return Message.reply("sensor-list", "ok",
                                     String.valueOf(numSensors));
            }
            else {
                String name = args[0];
                if (sensors.containsKey(name)) {
                    sensor = sensors.get(name);
                    sensor.read();

                    msgArgs.clear();
                    msgArgs.add(sensor.getName());
                    msgArgs.add(sensor.getType().katcpName());
                    msgArgs.add(sensor.getDescription());
                    msgArgs.add(sensor.getUnits());
                    for (String param: sensor.getParameters()) {
                        msgArgs.add(param);
                    }

                    server.inform(sc, new Message(Message.Type.INFORM,
                                        "sensor-list",
                                        msgArgs.toArray(new String[0])));
                    return Message.reply("sensor-list", "ok", "1");
                }
                else {
                    return Message.reply("sensor-list", "fail",
                            "Unknown sensor name.");
                }
            }
        }
    }

    class SensorSamplingRequest extends Request {
        public SensorSamplingRequest(Server server) {
            super(server, "sensor-sampling", "Configure or query the way a"
                                             + " sensor is sampled.");
        }

        public Message handle(SocketChannel sc, Message msg) {
            String[] args = msg.getArguments();
            String[] params;
            String sensorName;
            String strategyName;
            List<String> msgArgs = new LinkedList<String>();
            List<String> strategyArgs;
            ISamplingStrategy strategy;
            ISamplingStrategy.Strategy strategyType;
            ISensor sensor;

            if (args.length == 0) {
                return Message.reply("sensor-sampling", "fail",
                                     "No sensor name given.");
            }

            sensorName = args[0];
            if (!sensors.containsKey(sensorName)) {
                return Message.reply("sensor-sampling", "fail",
                                     "Unknown sensor name.");
            }

            sensor = sensors.get(sensorName);

            if (args.length > 1) {
                // attempt to set sampling strategy
                strategyName = args[1];
                strategyType = ISamplingStrategy.Strategy.fromKatcpName(strategyName);
                strategyArgs = Arrays.asList(args);
                strategyArgs = strategyArgs.subList(2, strategyArgs.size());
                params = strategyArgs.toArray(new String[0]);
                strategy = StrategyBase.strategyFactory(sensor, strategyType, params);
                sensor.setSampling(strategy);
            }

            strategy = sensor.getSampling();
            strategyName = strategy.getType().katcpName();
            params = strategy.getStrategyParameters();

            msgArgs.add("ok");
            msgArgs.add(sensorName);
            msgArgs.add(strategyName);
            for (String param: params) {
                msgArgs.add(param);
            }

            return new Message(Message.Type.REPLY,
                                        "sensor-sampling",
                                        msgArgs.toArray(new String[0]));
        }
    }

    class WatchdogRequest extends Request {
        public WatchdogRequest(Server server) {
            super(server, "watchdog", "Check that the server is still alive.");
        }
        public Message handle(SocketChannel sc, Message msg) {
            return Message.reply("watchdog", "ok");
        }
    }

}
